﻿using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using IronPython.Hosting;
using Microsoft.CSharp;
using Microsoft.Scripting.Hosting;
using System.Collections.Specialized;
using System.Diagnostics;

namespace Kyoh.Lib.Plugin
{
    /// <summary>プラグインの読み込み（DLL、C#、IronPython）を行うコンポーネント。</summary>
    /// <typeparam name="T">読み込み対象のプラグインの基本となるインターフェースの型を指定します。</typeparam>
    public class PluginLoader<T> : IDisposable
        where T : class, IPlugin
    {
        private long domainIndex = 0;
        private System.ComponentModel.IContainer components = null;
        private CSharpCodeProvider cscp = null;
        private ScriptEngine pythonEngine = null;

        private Dictionary<Guid, Assembly> _assemblies = null;
        private Dictionary<Assembly, int> _assembyReferences = null;
        private Dictionary<Guid, ScriptScope> _scopes = null;

        private SortedSet<string> _pathes = null;

        /// <summary>PluginLoader{T} の新しいインスタンスを生成します。</summary>
        public PluginLoader()
        {
            components = new System.ComponentModel.Container();
            cscp = new CSharpCodeProvider();
            CompilerParameters = new CompilerParameters() { GenerateInMemory = true };
            components.Add(cscp);

            pythonEngine = Python.CreateEngine();

            _assemblies = new Dictionary<Guid, Assembly>();
            _assembyReferences = new Dictionary<Assembly, int>();
            _scopes = new Dictionary<Guid, ScriptScope>();

            _pathes = new SortedSet<string>(StringComparer.InvariantCultureIgnoreCase);

            AppDomain.CurrentDomain.ResourceResolve += new ResolveEventHandler(CurrentDomain_ResourceResolve);
        }

        private Assembly CurrentDomain_ResourceResolve(object sender, ResolveEventArgs args)
        {
            Debug.WriteLine(args.Name);
            return args.RequestingAssembly;
        }

        #region Dispose

        /// <summary>使用中のリソースをすべてクリーンアップします。</summary>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>使用中のリソースをすべてクリーンアップします。</summary>
        ~PluginLoader()
        {
            Dispose(false);
        }

        /// <summary>使用中のリソースをすべてクリーンアップします。</summary>
        /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
        protected void Dispose(bool disposing)
        {
            if (disposing && (components != null))
                components.Dispose();
        }

        #endregion

        public CompilerParameters CompilerParameters { get; private set; }

        /// <summary>指定されたファイルパスから、<typeparamref name="T"/> 型のプラグインインスタンスを取得します。</summary>
        /// <param name="path">DLL、C#、IronPythonのいずれかのパス。</param>
        /// <returns>読み込まれたプラグインインスタンス。</returns>
        public IEnumerable<T> Load(string path)
        {
            if (path == null)
                throw new ArgumentNullException("path");
            path = path.ToLower();
            if (path.EndsWith(".dll"))
                return LoadDll(path);
            else if (path.EndsWith(".cs"))
                return LoadCSharp(path);
            else if (path.EndsWith(".py"))
                return LoadPython(path);
            else
                return null;
        }

        /// <summary>指定されたプラグインをアンロードします。</summary>
        /// <param name="plugin">プラグインのインスタンス。</param>
        public void Unload(T plugin)
        {
            if (plugin == null)
                throw new ArgumentNullException("plugin");
            Guid guid = plugin.GetType().GUID;
            Assembly assembly;
            if (_assemblies.TryGetValue(guid, out assembly))
            {
                if (--_assembyReferences[assembly] < 1)
                    _assembyReferences.Remove(assembly);
                _assemblies.Remove(guid);
            }
            else if (_scopes.ContainsKey(guid))
                _scopes.Remove(guid);
        }

        /// <summary>すべてのプラグインをアンロードします。</summary>
        public void Clear()
        {
            _assemblies.Clear();
            _assembyReferences.Clear();
            _scopes.Clear();
        }

        private IEnumerable<T> LoadDll(string path)
        {
            var dllDir=Path.GetDirectoryName(path);
            if (!_pathes.Contains(dllDir))
            {
                _pathes.Add(dllDir);
                Environment.SetEnvironmentVariable("Path", Environment.GetEnvironmentVariable("Path") + ";\"" + dllDir + '"');
            }

            var image = File.ReadAllBytes(path);
            var assem = Assembly.Load(image);
            foreach (var instance in CreateInstances(assem, false))
            {
                _assemblies.Add(instance.GetType().GUID, assem);
                if (!_assembyReferences.ContainsKey(assem))
                    _assembyReferences.Add(assem, 0);
                _assembyReferences[assem]++;
                yield return instance;
            }

        }

        private IEnumerable<T> LoadCSharp(string path)
        {
            var results = cscp.CompileAssemblyFromSource(CompilerParameters, File.ReadAllText(path));
            
            return CreateInstances(results.CompiledAssembly, false);
        }

        private IEnumerable<T> LoadPython(string path)
        {
            var pythonScope = pythonEngine.CreateScope();

            // Python スクリプトを実行
            pythonEngine.CreateScriptSourceFromFile(path).Execute(pythonScope);

            // Python スクリプト内で定義したクラスのインスタンスを取得
            IEnumerable<T> instances;
            try
            {
                instances = pythonScope.GetVariable<IEnumerable<T>>("instances");
            }
            catch (MissingMemberException) { yield break; }
            if (instances == null)
                yield break;
            foreach (var instance in instances)
            {
                // ScriptScope を キャッシュに登録
                Unload(instance);
                _scopes.Add(instance.GetType().GUID, pythonScope);

                yield return instance;
            }
        }

        /// <summary>アセンブリから単一のプラグインインスタンスを生成します。</summary>
        /// <param name="assembly">生成元のアセンブリ。</param>
        /// <returns>生成されたプラグインインスタンス。</returns>
        public static T CreateInstance(Assembly assembly)
        {
            Type t = assembly.GetTypes().FirstOrDefault(Contract);
            return t == null ? null : (T)Activator.CreateInstance(t);
        }

        /// <summary>アセンブリから複数のプラグインインスタンスを生成します。</summary>
        /// <param name="assembly">生成元のアセンブリ。</param>
        /// <returns>生成されたプラグインインスタンス群。</returns>
        public static IEnumerable<T> CreateInstances(Assembly assembly, bool serializable)
        {
            return assembly.GetTypes().Where(serializable ? new Func<Type, bool>(ContractSerializable) : new Func<Type, bool>(Contract)).Select(Activator.CreateInstance).Where(t => t != null).OfType<T>();
        }

        private static bool Contract(Type type)
        {
            return typeof(T).IsAssignableFrom(type) && type.IsClass && !type.IsAbstract && type.IsPublic;
        }

        private static bool ContractSerializable(Type type)
        {
            return typeof(T).IsAssignableFrom(type) && type.IsClass && type.IsMarshalByRef && !type.IsAbstract && type.IsPublic;
        }
    }
}